/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package axil.framework.validation;

import axil.etc.Identity;
import axil.etc.Immutable;
import axil.framework.localization.Message;
import axil.framework.validation.ValidationMessage.Severity;
import axil.stdlib.collection.type.Range;

import java.util.*;

import static axil.framework.Functions.*;


/**
 * The object that is passed down through the validation of a complex object,
 * retaining the contextual chain of where problems occurred.
 */
public class ValidationContext {
	private ArrayList<ValidationMessage> messages;
	private Severity limit;
	private Severity actual;
	private Location path;
	private Map<String,Object> map;


	public ValidationContext() {
		this(Severity.info);
	}


	public ValidationContext(Severity limit) {
		this.messages = null;
		this.actual = Severity.info;
		this.limit = limit;
		this.path = null;
		this.map = null;
	}


	/**
	 * Tell if this context indicates validation succeeded. This returned a
	 * true value as long as any messages added are within the acceptable
	 * tolerance established when the context was created.
	 */
	public boolean ok() {
		return actual.within(limit);
	}


	public Severity severity() {
		return actual;
	}


	public java.util.List messages() {
		if (messages == null) {
			return Collections.emptyList();
		}
		return messages;
	}



	/**
	 * Put an object into this validation context so that it can be referenced
	 * later on in the validation process. This is only used for "out of band"
	 * data, not for the object that are themselves being validated.
	 *
	 * Neither the key nor the value can be null. If there is already an object
	 * with this key, then that is considered an error; this is designed to
	 * prevent subtle bugs where the values in the context are not what you
	 * think they are. Use remove() first if this is a problem for you.
	 */
	public void put(String key, Object value) {
		assert ! empty(key);
		assert value != null;

		if (map == null) {
			map = new HashMap<String, Object>();
		}
		if (map.put(key, value) != null) {
			throw abort("There is already an object in the validation context " +
			            "with that key.",
			            nv("key", key), nv("value", value));
		}
	}


	/**
	 * Put an object into this validation context so that it can be referenced
	 * later on in the validation process. This is only used for "out of band"
	 * data, not for the object that are themselves being validated.
	 *
	 * The object given cannot be null. It is registered in this context with
	 * the full name of the class of the value. If there is already an object
	 * with this key, then that is considered an error; this is designed to
	 * prevent subtle bugs where the values in the context are not what you
	 * think they are. Use remove() first if this is a problem for you.
	 */
	public void put(Object value) {
		put(value.getClass().getName(), value);
	}


	/**
	 * Remove an object from the context identified by the given key. If there
	 * is no such object registered, nothing happens.
	 */
	public void remove(String key) {
		map.remove(key);
	}


	/**
	 * Remove an object from the context identified by the full class name of
	 * the given value. If there is no such object registered, nothing happens.
	 */
	public void remove(Object value) {
		remove(value.getClass().getName());
	}


	/**
	 * Get an object that is specific to the host application into this
	 * configuration. If there is no object associated with that key, then an
	 * exception is thrown.
	 */
	public Object get(String key) {
		return require(map.get(key));
	}


	/**
	 * Get an object that is specific to the host application into this
	 * configuration. If there is no object associated with that key, then an
	 * exception is thrown.
	 */
	public <T> T get(Class<T> type) {
		return (T)get(type.getName());
	}


	private void put(ValidationMessage message) {
		actual = actual.worse(message.severity());
		if (messages == null) {
			messages = new ArrayList<ValidationMessage>();
		}
		messages.add(message);
	}


	/**
	 * Unconditionally add an error to this validation context for the given
	 * member within the current context.
	 */
	public boolean error(String member, Message message) {
		put(new ValidationMessage(new Location(path, member),
		                          Severity.error, message));
		return false;
	}


	/**
	 * Unconditionally add an error to this validation context for the current
	 * object within this context.
	 */
	public boolean error(Message message) {
		put(new ValidationMessage(path, Severity.error, message));
		return false;
	}


	/**
	 * Check the given condition and add an error message for the given field
	 * if the condition is false.
	 */
	public boolean check(String member, boolean condition, Message message) {
		return ! condition ? error(member, message) : true;
	}


	/**
	 * Check if the given string is not null and not empty. If that condition
	 * fails, then add a generic error message for the given member.
	 */
	public boolean required(String member, String value) {
		if (empty(value)) {
			put(new ValidationMessage(
			        new Location(path, member),
			        Severity.error,
			        new Message(axil(), "not-blank")));
			return false;
		}
		return true;
	}


	public boolean required(String member, Object value) {
		if (value == null) {
			put(new ValidationMessage(
			        new Location(path, member),
			        Severity.error,
			        new Message(axil(), "value-required")));
			return false;
		}
		return true;
	}


	public boolean validate(String member, Validated object) {
		return validate(member, object, false);
	}


	public boolean validate(String member, Validated object, boolean nullable) {
		boolean ok = true;
		path = new Location(path, member);
		if (object == null) {
			if (! nullable) {
				put(new ValidationMessage(path, Severity.error,
				                          new Message(axil(), "value-required")));
				ok = false;
			}
		} else {
			ok = check(object);
		}
		path = path.rest();
		return ok;
	}


	private boolean check(Validated object) {
		int save = save();
		object.validate(this);
		return to(save);
	}


	private int save() {
		return messages == null ? -1 : messages.size() - 1;
	}


	private boolean to(int save) {
		if (messages != null) {
			for (int i = messages.size() - 1; i > save; i --) {
				ValidationMessage m = messages.get(i);
				if (! m.severity().within(limit)) {
					return false;
				}
			}
		}
		return true;
	}


	public <T extends Validated> boolean validate(String member,
	        Collection<? extends Validated> objects) {
		return validate(member, objects, Range.some);
	}


	public boolean validate(String member,
	                        Collection<? extends Validated> objects, Range range) {
		if ((objects == null) || ! range.bounds(objects)) {
			return error(member, new Message(axil(), "not-enough-values"));
		}

		boolean ok = true;
		int i = 0;
		for (Validated v : objects) {
			path = location(member, v, i);
			if (! check(v)) {
				ok = false;
			}
			path = path.rest();
			i++;
		}
		return ok;
	}


	private Location location(String member, Validated object, int index) {
		if ((object != null) && (object instanceof Identity)) {
			return new Location(path, member, ((Identity)object).identity());
		}
		return new Location(path, member, index);
	}


	public <T extends Validated & Immutable> boolean validate(String member,
	        Iterable<T> objects) {
		return validate(member, objects, Range.some);
	}


	public <T extends Validated & Immutable> boolean validate(String member,
	        Iterable<T> objects, Range range) {
		if ((objects == null) || ! range.bounds(size(objects))) {
			return error(member, new Message(axil(), "not-enough-values"));
		}

		boolean ok = true;
		int i = 0;
		for (Validated v : objects) {
			path = location(member, v, i);
			if (! check(v)) {
				ok = false;
			}
			path = path.rest();
			i++;
		}
		return ok;
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		return single(strings(messages()));
	}
}
